{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Chat dialog\n",
    "\n",
    "Guidance supports chat-based models using role tags. These are then converted to the appropriate format for the model (either a JSON API format or special tokens)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "parameters"
    ]
   },
   "outputs": [],
   "source": [
    "call_delay_secs = 0\n",
    "requested_log_level = logging.WARNING"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logging.basicConfig(level=requested_log_level)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Preliminaries concluded, we can now create our model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from guidance import models, gen\n",
    "\n",
    "chat_enabled_model = models.Transformers(\"microsoft/Phi-4-mini-instruct\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Multi-step chat with hidden blocks"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We are now going to set up a multistage chat, where we have the chat bot help the use achieve some goal.\n",
    "The user will only have to specify the goal, and then we will create a chain-of-thought conversation with the bot which will:\n",
    "\n",
    "1. Ask the bot for a number of suggestions.\n",
    "2. List the pros and cons of each.\n",
    "3. Pick the best suggestion.\n",
    "4. Product a detailed action plan.\n",
    "\n",
    "Our goal is to only show the final result to the user."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, let us define our generation function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import re\n",
    "import time\n",
    "\n",
    "import guidance\n",
    "from guidance import gen, select, system, user, assistant\n",
    "\n",
    "@guidance\n",
    "def plan_for_goal(lm, goal: str):\n",
    "    \n",
    "    # This is a helper function which we will use below\n",
    "    def parse_best(prosandcons, options):\n",
    "        best = re.search(r'Best=(\\d+)', prosandcons)\n",
    "        if not best:\n",
    "            best =  re.search(r'Best.*?(\\d+)', 'Best= option is 3')\n",
    "        if best:\n",
    "            best = int(best.group(1))\n",
    "        else:\n",
    "            best = 0\n",
    "        return options[best]\n",
    "\n",
    "    # Some general instruction to the model\n",
    "    with system():\n",
    "        lm += \"You are a helpful assistant.\"\n",
    "\n",
    "    # Simulate a simple request from the user\n",
    "    # Note that we switch to using 'lm2' here, because these are intermediate steps (so we don't want to overwrite the current lm object)\n",
    "    with user():\n",
    "        lm2 = lm + f\"\"\"\\\n",
    "        I want to {goal}\n",
    "        Can you please generate one option for how to accomplish this?\n",
    "        Please make the option very short, at most one line.\"\"\"\n",
    "\n",
    "    # Generate several options. Note that this means several sequential generation requests\n",
    "    n_options = 5\n",
    "    with assistant():\n",
    "        options = []\n",
    "        for i in range(n_options):\n",
    "            options.append((lm2 + gen(name='option', temperature=1.0, max_tokens=50))[\"option\"])\n",
    "\n",
    "    # Have the user request pros and cons\n",
    "    with user():\n",
    "        lm2 += f\"\"\"\\\n",
    "        I want to {goal}\n",
    "        Can you please comment on the pros and cons of each of the following options, and then pick the best option?\n",
    "        ---\n",
    "        \"\"\"\n",
    "        for i, opt in enumerate(options):\n",
    "            lm2 += f\"Option {i}: {opt}\\n\"\n",
    "        lm2 += f\"\"\"\\\n",
    "        ---\n",
    "        Please discuss each option very briefly (one line for pros, one for cons), and end by saying Best=X, where X is the number of the best option.\"\"\"\n",
    "\n",
    "    # Get the pros and cons from the model\n",
    "    with assistant():\n",
    "        lm2 += gen(name='prosandcons', temperature=0.0, max_tokens=600, stop=\"Best=\") + \"Best=\" + gen(\"best\", regex=\"[0-9]+\")\n",
    "        time.sleep(call_delay_secs)\n",
    "\n",
    "    # The user now extracts the one selected as the best, and asks for a full plan\n",
    "    # We switch back to 'lm' because this is the final result we want\n",
    "    with user():\n",
    "        lm += f\"\"\"\\\n",
    "        I want to {goal}\n",
    "        Here is my plan: {options[int(lm2[\"best\"])]}\n",
    "        Please elaborate on this plan, and tell me how to best accomplish it.\"\"\"\n",
    "\n",
    "    # The plan is generated\n",
    "    with assistant():\n",
    "        lm += gen(name='plan', max_tokens=500)\n",
    "        time.sleep(call_delay_secs)\n",
    "\n",
    "    return lm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create a plan for the user. Note how the portions which were sent to `lm2` in the function above are not shown in the final result:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "results = chat_enabled_model + plan_for_goal(goal=\"read more books\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can access the final plan itself:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(results['plan'])"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Asking help from experts\n",
    "\n",
    "Now, let us ask our chat model to pick some experts in a particular field, and impersonate them to give advice:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@guidance\n",
    "def run_expert_advice(lm, query: str):\n",
    "    # Some general instruction to the model\n",
    "    with system():\n",
    "        lm += \"You are a helpful assistant.\"\n",
    "\n",
    "    with user():\n",
    "        lm += f\"\"\"I want a response to the following question:\n",
    "{query}\n",
    "Who are 3 world-class experts (past or present) who would be great at answering this?\n",
    "Please don't answer the question or comment on it yet.\n",
    "\"\"\"\n",
    "\n",
    "    with assistant():\n",
    "        lm += gen(name='experts', temperature=0, max_tokens=300)\n",
    "        time.sleep(call_delay_secs)\n",
    "\n",
    "    with user():\n",
    "        lm += \"\"\"Great, now please answer the question as if these experts had collaborated in writing a joint anonymous answer.\n",
    "In other words, their identity is not revealed, nor is the fact that there is a panel of experts answering the question.\n",
    "If the experts would disagree, just present their different positions as alternatives in the answer itself (e.g. 'some might argue... others might argue...').\n",
    "Please start your answer with ANSWER:\n",
    "\"\"\"\n",
    "\n",
    "    with assistant():\n",
    "        lm += gen(name='answer', temperature=0, max_tokens=500)\n",
    "        time.sleep(call_delay_secs)\n",
    "\n",
    "    return lm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mean_life = chat_enabled_model + run_expert_advice(\"What is the meaning of life?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "more_productive = chat_enabled_model + run_expert_advice('How can I be more productive?')"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Agents\n",
    "\n",
    "We are now going to define a 'conversation agent.'\n",
    "This maintains a memory of a conversation, and can generate an appropriate reply, based on the persona it has been given."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ConversationAgent:\n",
    "    def __init__(self, chat_model, name: str, instructions: str, context_turns: int = 2):\n",
    "        self._chat_model = chat_model\n",
    "        self._name = name\n",
    "        self._instructions = instructions\n",
    "        self._my_turns = []\n",
    "        self._interlocutor_turns = []\n",
    "        self._went_first = False\n",
    "        self._context_turns = context_turns\n",
    "\n",
    "    @property\n",
    "    def name(self) -> str:\n",
    "        return self._name\n",
    "    \n",
    "    def reply(self, interlocutor_reply = None) -> str:\n",
    "        if interlocutor_reply is None:\n",
    "            self._my_turns = []\n",
    "            self._interlocutor_turns = []\n",
    "            self._went_first = True\n",
    "        else:\n",
    "            self._interlocutor_turns.append(interlocutor_reply)\n",
    "\n",
    "        # Get trimmed history\n",
    "        my_hist = self._my_turns[(1-self._context_turns):]\n",
    "        interlocutor_hist = self._interlocutor_turns[-self._context_turns:]\n",
    "\n",
    "        # Set up the system prompt\n",
    "        curr_model = self._chat_model\n",
    "        with system():\n",
    "            curr_model += f\"Your name is {self.name}. {self._instructions}\"\n",
    "            if len(interlocutor_hist) == 0:\n",
    "                curr_model += \"Introduce yourself and start the conversation\"\n",
    "            elif len(interlocutor_hist) == 1:\n",
    "                curr_model += \"Introduce yourself before continuing the conversation\"\n",
    "\n",
    "        # Replay the last few turns\n",
    "        for i in range(len(my_hist)):\n",
    "            with user():\n",
    "                curr_model += interlocutor_hist[i]\n",
    "            with assistant():\n",
    "                curr_model += my_hist[i]\n",
    "\n",
    "        if len(interlocutor_hist) > 0:\n",
    "            with user():\n",
    "                curr_model += interlocutor_hist[-1]\n",
    "\n",
    "        with assistant():\n",
    "            curr_model += gen(name='response', max_tokens=100)\n",
    "        time.sleep(call_delay_secs)\n",
    "\n",
    "        self._my_turns.append(curr_model['response'])\n",
    "        return curr_model['response']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can have two of these agents converse with each other with a _conversation simulator_:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def conversation_simulator(\n",
    "    bot0: ConversationAgent,\n",
    "    bot1: ConversationAgent,\n",
    "    total_turns: int = 5 ):\n",
    "    conversation_turns = []\n",
    "    last_reply = None\n",
    "    for _ in range(total_turns):\n",
    "        last_reply = bot0.reply(last_reply)\n",
    "        conversation_turns.append(dict(name=bot0.name, text=last_reply))\n",
    "        time.sleep(call_delay_secs)\n",
    "        last_reply = bot1.reply(last_reply)\n",
    "        conversation_turns.append(dict(name=bot1.name, text=last_reply))\n",
    "    return conversation_turns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, let's try generating a conversation:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bot_instructions = \"\"\"You are taking part in a discussion about bodyline bowling.\n",
    "Only generate text as yourself and do not prefix your reply with your name.\n",
    "Keep your answers to a couple of short sentences.\"\"\"\n",
    "\n",
    "bradman_bot = ConversationAgent(chat_enabled_model, \"Donald Bradman\", bot_instructions, context_turns=5)\n",
    "jardine_bot = ConversationAgent(chat_enabled_model, \"Douglas Jardine\", bot_instructions, context_turns=5)\n",
    "\n",
    "conversation_turns = conversation_simulator(bradman_bot, jardine_bot, total_turns=3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for turn in conversation_turns:\n",
    "    print(f\"{turn['name']}: {turn['text']}\\n\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<hr style=\"height: 1px; opacity: 0.5; border: none; background: #cccccc;\">\n",
    "<div style=\"text-align: center; opacity: 0.5\">Have an idea for more helpful examples? Pull requests that add to this documentation notebook are encouraged!</div>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
